UMMAH FLOWS · 01 Verified against code · Aug 2026

Pricing architecture · flows & scenarios

Pricings, Splits & Fees

How Ummah prices a merchant, how a marketplace Client prices its sub-merchants, and how every payment divides itself inside the card authorisation — with worked numbers, the exact authority chain, per-scheme behaviour for Visa / Mastercard / Amex, and the architect's verdict on what stands, what bites, and what to change. Every claim below was verified in the platform code.

Section 1

The model in one view

Ummah does not invoice fees and it does not sweep commission at month-end. The money divides itself at the moment the card is authorised, on Adyen's balance platform, according to a template Ummah controls — the Split Profile. Everything else in this document is detail on who edits that template, how the legs land, and what happens when money flows backwards.

Where a payment goes

FIG 1 · money topology
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","secondaryColor":"#F1F4F8","tertiaryColor":"#F7F8FA","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":38,"rankSpacing":52,"padding":10}}}%%
flowchart LR
  shopper(["Donor taps card
£100.00"]) --> adyen["Adyen authorises &
auto-splits at source
store's split configuration"] adyen -->|"remainder
£96.00"| store[("Sub-merchant
store balance account")] adyen -->|"commission
£2.00"| liable[("Ummah
liable balance account")] adyen -->|"additionalCommission
£2.00"| client[("Marketplace Client
balance account")] adyen -.->|"paymentFee — Adyen's own cost"| bearer{"feesBorneBy?"} bearer -.->|"SUBMERCHANT
passthrough"| store bearer -.->|"PLATFORM
blended"| liable classDef adyen fill:#00273A,stroke:#00273A,color:#FFFFFF; classDef liable fill:#E86C2B,stroke:#B84E1F,stroke-width:1.5px,color:#FFFFFF; classDef client fill:#E8F0FE,stroke:#1A73B8,stroke-width:1.5px,color:#00273A; classDef sub fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class adyen adyen; class liable liable; class client client; class store sub; class shopper,bearer plain;
Ummah Marketplace Client Merchant / sub-merchant funds Adyen rails
The numbers shown are the platform's own verified TEST payment (PSP TZGGMW3F9CWD2HV5): £100 with Ummah at 1% + £1.00 and the Client's cut at 2% booked Seller £96.00 · Ummah £2.00 · Client £2.00 — three parties paid from one card tap, no invoices.

SplitProfile — payment pricing

The only live mechanism that prices payments. Commission (% + fixed) to Ummah, an optional second cut to a marketplace Client, a fee-bearer switch, and optional per-scheme rates. Synced to Adyen as a store-attached split configuration; a store without one cannot take payments at all (422 store_not_split).

FeeProfile — event pricing

Prices non-payment events — TRANSFER, CHARGEBACK, DECLINE, REFUND — as Adyen internal transfers booked by the worker into Ummah's liable account, with an optional Client cut on top for sub-merchants. Payouts are free by decision (2026-08-11); the PAYOUT enum value is reserved.

Calculator — pricing intelligence

Resolves the exact Ummah/Client cuts from the store's live profile (Adyen's own half-to-even rounding) and estimates card costs from the admin-editable PlatformCostAssumption rate card. Warns in red when a blended profile loses money on a card. Staff and merchants see the same engine.

A fourth mechanism — the per-method FeeSchedule engine — was retired in place behind FEATURE_PRICING (its routes 404 by default). It still silently computes the frozen snapshot every checkout session; only profile-less stores would ever use it, and the checkout guard makes those unreachable. It appears in this document only as a failure-mode footnote.

The deep reference — the comprehensive internal pricing bible, Ummah Pay — Fees, Card Types, Currencies & Pricing, carries the full cost-by-card-type taxonomy, the split fee groups, the currency/FX scenarios and the worked £100 examples this document builds on. Section 7 works out what a true IC++ product would mean on the split engine.

Section 2

The vocabulary

Eight terms carry the whole pricing conversation. Precise meanings, as implemented:

TermWhat it means in Ummah
CommissionUmmah's cut of a payment: commissionPercentBps (basis points, 115 = 1.15%) + commissionFixedMinor (pence). Emitted as Adyen splitLogic.commission, which books automatically to Ummah's liable balance account — no account id needed.
Additional commissionThe marketplace Client's cut on its sub-merchants' payments: additionalCommission* + the Client's own balance-account id. The third leg of the 3-way split. One Client per profile; only the rate can vary per scheme.
RemainderWhat's left after the cuts — the merchant's net. Adyen adds it to the paid store's balance account (addToOneBalanceAccount).
Fee bearer feesBorneByWho pays Adyen's real processing costs (interchange + scheme + markup). SUBMERCHANT = deducted from the paid store's account (passthrough — merchant pays actuals, Ummah's commission is clean margin). PLATFORM = deducted from the liable account (blended — Ummah absorbs costs inside its commission).
Method groupsPer-scheme pricing in exactly three buckets: VISA_MC (one rate for both), AMEX, and REMAINING — which doubles as the mandatory catch-all: Adyen rule ANY/ANY. Without a catch-all, an unmatched payment books 100% to the liable account and the merchant receives nothing.
Liable accountUmmah's own balance account on the Adyen balance platform (TrustXPay operating entity). Receives every commission leg and every fee charge — and is also what Adyen debits for chargebacks. Commission income and absorbed losses share this account, which is why the ledger must keep them apart.
The mirror splitsAppliedUmmah never sends split amounts on a profile-store payment — Adyen computes them. The platform records its own prediction of the legs (same names, same banker's rounding) on the Transaction, then reconciles it against the legs Adyen actually booked. Agreement ⇒ SETTLED; divergence ⇒ splitsMismatch + ops alarm.
FeeChargeThe deduction ledger for FeeProfile events: one row per charge with a unique sourceRef (e.g. chargeback-<disputeId>), booked as up to two idempotent internal transfers (platform leg → liable account; Client leg → parent's account), retried until the money lands.

Section 3

Who sets what

The authority chain is deliberate and asymmetric: a merchant's own economics are always Ummah's to set; a Client composes its own cut on top and can never touch Ummah's. This is enforced in the DTOs, not the UI — the portal API physically cannot carry Ummah's rate.

The pricing authority chain

FIG 2 · who configures whom
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":34,"rankSpacing":48,"padding":10}}}%%
flowchart TB
  subgraph BO["Ummah back-office · STAFF scope"]
    admin["Ummah admin"]
  end
  subgraph PORTAL["Merchant portal · MERCHANT scope"]
    clientUser["Marketplace Client"]
  end
  admin -->|"full SplitProfile CRUD + sync + assign
api/admin/split-profiles"| profiles["Split profiles
any merchant, any store"] admin -->|"defaultSplitProfileId — mandatory at invite
merchants never choose their own"| direct["Direct merchant pricing"] admin -->|"subMerchantCommission% + £ — LOCKED
set when enabling sub-merchants"| lock["Ummah's cut on every
sub-merchant payment"] admin -->|"FeeProfile per merchant
transfer · chargeback · decline · refund"| fees["Non-payment fees"] clientUser -->|"cutPercentBps + cutFixedMinor ONLY
api/portal/split-profiles"| cut["Client's own cut
on its subs' payments"] clientUser -->|"assign / unassign to its subs' stores
+ per-sub caps & refund bearer"| assign["Sub-merchant
store assignment"] lock -.->|"stamped server-side into every
Client-created profile, re-stamped on every sync"| cut sub["Sub-merchant"] ---|"read-only: calculator + per-payment split legs"| assign classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef client fill:#E8F0FE,stroke:#1A73B8,stroke-width:1.5px,color:#00273A; classDef sub fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class admin,profiles,direct,lock,fees ummah; class clientUser,cut,assign client; class sub sub;
The dashed edge is the crux: the Client's create/edit surface accepts only its own cut. Ummah's commission is stamped from the admin-set Merchant.subMerchantCommission* fields on create and re-stamped on every Adyen sync, so a tampered or stale value can never reach the processor.
Authority matrix — every pricing control and who holds it
ControlUmmah adminMarketplace ClientMerchant / sub-merchant
Own-store pricing (any merchant)Sets at invite (mandatory defaultSplitProfileId), reassigns per storeNever — portal store-create ignores splitProfileIdNever
Ummah's cut on sub-merchant paymentsSets at invite; locked; edits propagate on syncRead-only label on /splitsSees it only inside calculator output
Client's cut on sub-merchant paymentsCan set, including per-scheme ratesYes — single rate (% + £) per profileNo
Fee bearer (feesBorneBy)Chooses PLATFORM or SUBMERCHANTForced to SUBMERCHANT on portal-created profiles—
Per-scheme rates (Visa/MC · Amex · rest)Yes — three-bucket editor in the back-officeNo — portal DTO is single-rate—
Non-payment fees (FeeProfile)Yes — CRUD + per-merchant assignmentNo surface existsNo surface exists
Refund fee bearerrefundFeeBearer per merchant (MERCHANT | PLATFORM)clientRefundFeeBearer per sub (SUBMERCHANT | CLIENT)—
Approval caps (payout / refund / MIT)Per merchantPer sub-merchant (payout + refund)—
Cost assumptions (calculator rate card)Yes — no deploy needed——

Why lock Ummah's cut instead of trusting the Client?

Because the Client's profile is synced to Adyen by the Client. If the portal DTO carried Ummah's rate, a compromised or buggy client could sync a 0% Ummah commission into the live split configuration. Stamping server-side from an admin-owned field — and re-stamping on every sync — makes the locked rate self-healing: even a bad row in the database is corrected the next time the profile touches Adyen. This is the right design; keep it.

Two-level control — who prices whom, across the two portals

Pricing is set in two places by two different actors, and the split of powers is deliberate. The back-office (Ummah staff) owns everything that is Ummah's revenue or Ummah's risk; the merchant portal (a marketplace Client) owns exactly one lever — its own cut on its sub-merchants. This is what the two screens actually expose:

Who sets pricing for whom

FIG 2A · the two portals
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":30,"rankSpacing":46,"padding":10}}}%%
flowchart TB
  subgraph BO["Back-office · Ummah staff · full control"]
    a1["For a merchant
Ummah commission on the
merchant's own stores"] a2["For sub-merchants
Ummah's LOCKED commission
+ per-scheme rates
+ fee-bearer (blended / passthrough)"] end subgraph PORTAL["Merchant portal · marketplace Client · one lever"] c1["Your cut % + £
flat, on its sub-merchants
Ummah's cut shown read-only"] end a2 -.->|"stamps & locks Ummah's cut
into every Client profile, on every sync"| c1 a1 --> mp["Merchant's own store payment"] a2 --> sp["Sub-merchant payment"] c1 --> sp sub["Sub-merchant"] -->|"read-only — sees the rate,
sets nothing"| sp classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef client fill:#E8F0FE,stroke:#1A73B8,stroke-width:1.5px,color:#00273A; classDef subm fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class a1,a2 ummah; class c1 client; class sub subm; class mp,sp plain;
The dashed edge is the crux: the Client's cut is the only field the portal accepts — Ummah's commission is stamped from an admin-locked value and re-stamped on every Adyen sync, so a Client can never move it. A direct merchant's own store pricing is set entirely by Ummah; the merchant has no pricing surface for its own stores at all.

The question this raises: a Client can only set a flat cut (one % + one £) — no per-scheme rates, no fee-bearer choice, no currency. Is that too little control? The answer is no, and for a non-obvious reason — but with two things that genuinely should change.

Why "flat cut only" is the right default

The back-office has a "price each scheme separately" toggle because of the Amex trap: Amex costs ~3.95%, so a single blended rate either loses money on Amex or overcharges every other card. But that danger only exists for whoever absorbs Adyen's real cost. A Client-created profile is forced to passthrough (feesBorneBy = SUBMERCHANT), so the sub-merchant bears the actual Adyen cost and the Client's cut sits on top as clean margin — never exposed to card-cost variance.

Why the Client needs no scheme-level control

FIG 2B · who carries the Amex risk
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":28,"rankSpacing":42,"padding":10}}}%%
flowchart LR
  q{"Who absorbs Adyen's
real card cost?"} q -->|"Ummah (blended)"| u["Exposed to the Amex trap
→ Ummah NEEDS per-scheme rates
+ the calculator's loss warning"] q -->|"Sub-merchant (passthrough)"| c["Client's cut is clean margin
→ flat cut is risk-free
per-scheme is a preference, not a need"] classDef warn fill:#FEF3DC,stroke:#B26A00,stroke-width:1.5px,color:#00273A; classDef ok fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class q plain; class u warn; class c ok;
The asymmetry is coherent: Ummah gets scheme-level control because its fee-bearing can be blended and carries the Amex risk; the Client doesn't, because passthrough shields it. Handing Clients the fee-bearer switch would let a marketplace offload its cost risk onto Ummah's liable account — which is exactly what stays locked.

What genuinely should change — bounds and transparency

The lack of per-scheme control for Clients is correct. Two other things are not:

  • No ceiling on the Client's cut. The portal accepts up to 100% + a large fixed amount with no admin-set bound — a marketplace Client could set a 99% cut and starve its own sub-merchants, with Ummah's brand and regulatory posture on the platform. The retired pricing engine even had this concept (childBounds); the live split engine dropped it. Fix: when the admin enables sub-merchants for a Client, set a maximum cut (% and £) the portal enforces. This is the real "should not be the case," and it is recommendation R6.
  • The Client can't reliably see Ummah's locked cut. The portal previews it from an arbitrary existing split, so a brand-new Client with no splits sees nothing — pricing blind against a number it can't read; and the sub-merchant never sees its all-in effective rate (Ummah + Client + passthrough estimate). Fix: always surface the admin-set Ummah commission (before the first split), and show the sub its all-in rate. This is recommendation R7.
Verdict: keep the two-level model exactly as it is — Ummah owns its commission and the fee-bearer; the Client owns only a flat cut, because passthrough makes that safe. Do not hand Clients scheme-level or fee-bearer control. Instead, add the bound on the Client's cut and the rate transparency — that is where the current design is genuinely incomplete. Per-scheme for a Client stays a staff action on request (the data model already supports it on the same profile).

Section 4

How a payment splits

The subtlety worth internalising: Ummah does not send split amounts on profile-store payments. It attaches the rules to the store once, then lets Adyen do the arithmetic on every authorisation — and audits Adyen leg-by-leg afterwards.

Payment, auto-split and reconciliation

FIG 3 · sequence
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13px","primaryColor":"#FBE5D6","primaryTextColor":"#00273A","primaryBorderColor":"#E86C2B","lineColor":"#8195A1","actorBkg":"#FFFFFF","actorBorder":"#D2D8DE","actorTextColor":"#00273A","actorLineColor":"#D2D8DE","signalColor":"#33505F","signalTextColor":"#33505F","activationBkgColor":"#FBE5D6","activationBorderColor":"#E86C2B","noteBkgColor":"#FEF3DC","noteBorderColor":"#E8D5A8","noteTextColor":"#00273A","labelBoxBkgColor":"#F1F4F8","labelBoxBorderColor":"#D2D8DE","loopTextColor":"#33505F"},"sequence":{"mirrorActors":false,"actorMargin":46,"messageMargin":30,"boxMargin":8}}}%%
sequenceDiagram
  participant M as Merchant server
  participant B as Ummah backend
  participant S as Shopper browser
  participant A as Adyen
  participant W as Ummah worker
  M->>B: POST /v1/checkout/sessions (sk_ key)
  B->>B: assertStoreIsSplit — profile + Adyen store + BA
else 422 store_not_split B-->>M: cs_ session token + pk_ key S->>B: POST /sdk/checkout/payments (card, brand hint) B->>B: record splitsApplied mirror
rates for the brand's scheme group B->>A: POST /payments — store=ST…, NO splits[] A->>A: auto-split per the store's
split configuration rules A-->>S: resultCode Authorised (3DS if needed) A--)W: webhook AUTHORISATION W->>W: Transaction → AUTHORIZED A--)W: balancePlatform.transfer.* — one event per booked leg W->>W: reconcileSplits: booked legs vs mirror alt legs sum & match W->>W: Transaction → SETTLED · reserve accrued
splitsReconciledAt stamped else divergence W->>W: splitsMismatch + audit + ops alarm end
The frozen per-method snapshot computed at session-create is only ever sent for profile-less stores — which the checkout guard makes unreachable. For profile stores it is the mirror (buildProfileSplits, half-to-even rounding to match Adyen exactly) that becomes splitsApplied.

Worked example — £100 at the standard rate

Ummah's standard UK rate is 1.15% + £0.15 (UK credit card, GBP, presented in the UK). On a £100 payment through a direct merchant's store:

What happens to Adyen's own cost (~£0.62 on this card) depends entirely on the fee bearer — which is the difference between Scenario 1 and Scenario 2 below.

Section 5

Scenarios

Ten scenarios cover the full space: direct merchants, the marketplace, money flowing backwards, and the ways the machine can bite. Each one states the configuration, walks the money, and closes with a verdict.

SCENARIO S1

Ummah prices a direct merchant — blended

Actor · Ummah admin commission 115 bps + 15p feesBorneBy = PLATFORM no additionalCommission

Setup. At invite, the admin must pick a defaultSplitProfileId — the invite refuses without one. On activation the default store inherits it; every later store inherits it too. The merchant never chooses, and never can.

What happens on £100 (UK Visa credit). Adyen books commission £1.30 to the liable account and remainder £98.70 to the store. Adyen's real cost (interchange + scheme + markup, ~£0.62) is deducted from the liable account — Ummah absorbs it inside the £1.30.

LineAmountLands where
Customer pays£100.00—
Merchant receives (remainder)£98.70Store balance account
Ummah commission 1.15% + £0.15£1.30Liable account
Adyen's actual cost (est.)−£0.62Deducted from the liable account
Ummah net margin£0.68Estimate — actuals are never captured (§7)

The Amex trap. A blended single rate must clear the worst card, not the average. Amex costs ~3.95% flat: on £100 that is ~£3.95 against £1.30 of commission — Ummah loses ~£2.65. The back-office calculator computes exactly this and raises its red ummahLoss alert. Blended profiles therefore demand either per-scheme rates (S3) or the passthrough posture (S2).

Works · with eyes openFully supported. But margin is an estimate: the platform never captures per-transaction interchange actuals, so "£0.68" exists only in the calculator, not in any ledger. See recommendation R8.

split-profiles.service.ts:465–573 · admin-merchants.service.ts:320–354 · calculator.engine.ts:21–145 (ummahLoss)

SCENARIO S2

Ummah prices a direct merchant — passthrough (the Tier-1 posture)

Actor · Ummah admin commission 20 bps + 10p feesBorneBy = SUBMERCHANT

Setup. The Tier-1 rate sheet is component-based: a locked £0.10 processing fee + a locked payment-method margin (0.20% on UK cards), with interchange and scheme fees passed through at Adyen's actual cost. In the engine this is one switch: feesBorneBy = SUBMERCHANT makes Adyen deduct its real costs from the merchant's store account, so Ummah's commission is clean margin.

LineAmountCharacter
Customer pays£100.00—
Ummah commission 0.20% + £0.10£0.30Locked — priced by Ummah
Interchange (UK credit, actual)~£0.30Passthrough — billed at cost
Scheme fee + Adyen markup (actual)~£0.43Passthrough — billed at cost
Merchant receives (approx.)~£98.97Varies with the shopper's actual card

Why the platform prefers this posture. The Phase-3 decision record is explicit: blended goes negative on Amex (−£2.58/£100) and even Visa credit at thin rates. Under passthrough, cost risk sits with the merchant and Ummah's margin is invariant per card. The sheet's per-line values for interchange are display estimates — Adyen deducts actuals.

Works · two caveatsThe mechanism is exactly right for Tier-1. But (a) the merchant's net becomes card-dependent and no merchant surface itemises the actuals they paid — statements show booked legs only; and (b) mixed postures (some lines locked, some passthrough within one profile) are all-or-nothing: feesBorneBy flips every Adyen cost at once. The Tier-1 sheet as written survives this; a future rate card with a locked scheme-fee line would not.

PRICING_TIER1_ALIGNMENT.md §2–3 · PHASE_3_SPLITS_PLAN.md D1 · statements.service.ts:24–338

SCENARIO S3

Per-scheme pricing — Visa/MC vs Amex vs everything else

Actor · Ummah admin perMethodPricing = true VISA_MC 115bps+15p AMEX 275bps+15p REMAINING 115bps+15p

Setup. The back-office form has a "price each card scheme separately" toggle that opens three rate cards. Sync writes one Adyen rule per scheme — visa and mc share the VISA_MC rate, amex gets its own, and ANY carries the REMAINING rate as the mandatory catch-all. Missing groups fall back to the profile's base rate, never to zero.

The same £100 on different cards, under this profile
Card presentedRule Adyen matchesUmmah feeMerchant net
Visa creditvisa → VISA_MC£1.30£98.70
Mastercard debitmc → VISA_MC£1.30£98.70
American Expressamex → AMEX£2.90£97.10
Apple Pay (Visa inside)ANY → REMAINING£1.30£98.70
Alipay / WeChat PayANY → REMAINING£1.30£98.70

The last two rows are the problem. Wallets and high-cost APMs share one bucket. The Tier-1 sheet wants wallets at 0.20% and Alipay/WeChat at 3.00% — those two rates cannot coexist in a three-bucket engine. The raw Adyen rules[] escape hatch can express the full grid, but it is invisible to the back-office UI and — verified in code — self-erasing: any later edit re-syncs the profile from the managed fields and silently deletes the raw rules at Adyen (S10).

Works · too coarse for Tier-1Amex-vs-Visa/MC pricing is solid and UI-managed. The three-bucket ceiling is the single biggest commercial gap: Tier-1 needs ~five buckets (cards+wallets · Amex · Amex US OptBlue · Alipay/WeChat · Pay by Bank). Recommendation R5 extends the enum properly.

schema.prisma:669–746 (SplitMethodGroup) · split-profiles.service.ts:527–573 (desiredRules) · SplitProfileFormDialog.tsx:607–767

SCENARIO S4

Ummah prices the whole marketplace — Client and sub-merchants

Actor · Ummah admin Beneficiary · Client Payer · Sub-merchant 3-way split, one configuration

Setup. When the admin invites a marketplace Client it makes three decisions in one dialog: the Client's own store pricing (its default profile), the canCreateSubMerchants capability, and — the number that matters most — Ummah's locked cut on every sub-merchant payment (subMerchantCommission% + £). Zero is legitimate: some Clients pay a one-off fee instead.

The admin can also build the sub-merchant profiles itself (with per-scheme rates if wanted — something the Client cannot do), setting the Client's cut as additionalCommission routed to the Client's balance account. Assignment is per sub-merchant store; a sub-merchant invite inherits the parent's default profile so the sub is never unsplit.

The 3-way split, as verified on Adyen TEST

FIG 4 · PSP TZGGMW3F9CWD2HV5
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":36,"rankSpacing":50,"padding":10}}}%%
flowchart LR
  pay(["£100.00 donation
sub-merchant's store"]) --> cfg["One split configuration
on the sub's store"] cfg -->|"seller remainder"| s[("Sub-merchant store BA
£96.00")] cfg -->|"commission — fixed £1.00
+ variable 1% £1.00"| u[("Ummah liable BA
£2.00")] cfg -->|"additionalCommission 2%"| c[("Client BA
£2.00")] classDef sub fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef liable fill:#E86C2B,stroke:#B84E1F,stroke-width:1.5px,color:#FFFFFF; classDef client fill:#E8F0FE,stroke:#1A73B8,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class s sub; class u liable; class c client; class pay,cfg plain;
One configuration produces all three legs because Adyen's commission needs no account (auto-books to the liable BA) while additionalCommission carries the Client's BA explicitly — a cross-account-holder booking Adyen accepts. Adyen allows one split configuration per store, and one is all it takes.
SoundThis is the strongest part of the machine: one template, three parties paid inside the authorisation, proven against Adyen's own ledger. When Ummah sets everything, the Client is a pure beneficiary and there is nothing it can misconfigure.

InviteMerchantDialog.tsx:278–335 · admin.dto.ts:83–107 · split-profiles.service.ts:465–513

SCENARIO S5

The Client prices its own sub-merchants

Actor · Marketplace Client POST api/portal/split-profiles cut% + cut£ only feesBorneBy forced SUBMERCHANT

What the Client actually controls. Its portal "Splits" page creates profiles with exactly two numbers — its own cut % and £. Everything else is decided for it: Ummah's commission is stamped from the locked rate (shown read-only with "Set by Ummah — you cannot change this"), the cut is routed to the Client's own balance account, the fee bearer is forced to passthrough. The Client then assigns the profile to a sub's store — with an invite-time picker, or later from /splits.

Client creates and deploys a sub-merchant split

FIG 5 · sequence with the server-side stamp
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13px","primaryColor":"#FBE5D6","primaryTextColor":"#00273A","primaryBorderColor":"#E86C2B","lineColor":"#8195A1","actorBkg":"#FFFFFF","actorBorder":"#D2D8DE","actorTextColor":"#00273A","actorLineColor":"#D2D8DE","signalColor":"#33505F","signalTextColor":"#33505F","activationBkgColor":"#FBE5D6","activationBorderColor":"#E86C2B","noteBkgColor":"#FEF3DC","noteBorderColor":"#E8D5A8","noteTextColor":"#00273A"},"sequence":{"mirrorActors":false,"actorMargin":46,"messageMargin":30}}}%%
sequenceDiagram
  participant C as Client (portal)
  participant B as Ummah backend
  participant A as Adyen Management API
  C->>B: POST /api/portal/split-profiles { name, cut 3% + £0 }
  B->>B: gate: canCreateSubMerchants + own BA provisioned
  B->>B: STAMP commission = admin-locked subMerchantCommission*
additionalCommission → Client's own BA
feesBorneBy := SUBMERCHANT · createdByMerchant := true C->>B: POST /:id/sync B->>B: re-stamp Ummah's commission from the current locked rate B->>A: create/update splitConfiguration
rules added before old ones deleted — catch-all never lapses A-->>B: splitConfigurationId C->>B: POST /:id/assign { storeId } B->>B: store must belong to one of THIS Client's subs
else 422 store_not_in_marketplace B->>A: PATCH store — attach configuration + store BA Note over C,A: every payment on that store now splits 3 ways automatically
Two independent guards prevent abuse: the ownership check on assignment (a Client-created profile can only ever attach inside that Client's marketplace — "would siphon another merchant's payments"), and the stamp that overwrites whatever commission value the row holds with the admin's locked rate on every sync.

What the Client cannot do — and whether that is right:

  • Price its own stores. Correct forever — that is Ummah's revenue line.
  • Vary its cut per scheme. An artificial limitation: staff can add per-scheme rules to the very same profile, so the data model supports it. Worth opening up once the buckets are extended (R10).
  • Choose the fee bearer. Correct — the sub pays acquiring costs; letting the Client flip costs onto Ummah's liable account would be a pricing grant, not a preference.
  • See Ummah's locked rate before creating its first split. A real bug-shaped gap: the portal previews it from an arbitrary existing profile, so a fresh Client sees nothing (R7).
Sound · with polish dueThe composition model — "the Client sets its own cut; Ummah's applies automatically underneath" — is the correct marketplace design and matches how the platform is sold. The gaps are ergonomic, not structural: no bounds on the Client's cut (it can set 99% + £10 and starve its own subs — see R6), no per-scheme cut, no rate preview.

portal-split-profiles.dto.ts:13–80 · split-profiles.service.ts:357–461, 606–632 · splits/page.tsx:204

SCENARIO S6

Ummah renegotiates the locked rate — what propagates, when

Actor · Ummah admin two edit paths, one truth

Path A — edit the merchant's locked fields (at invite, or via staff surfaces): the new subMerchantCommission* takes effect on each Client profile the next time that profile syncs. Nothing pushes automatically — a profile that never syncs again keeps charging the old rate at Adyen.

Path B — staff edits a Client-created profile directly (back-office deep-link): the service also rewrites the merchant's locked fields so the next sync doesn't revert the edit. Deliberate and correct — but it means editing one profile moves the locked rate for all of that Client's profiles at their next sync. The back-office warns about exactly this.

Correct · but implicitRe-stamp-on-sync is self-healing, yet propagation is lazy and invisible: there is no "resync all profiles of this Client" action and no audit trail of which profiles still carry the old rate at Adyen. R7 adds the missing surface: a dedicated admin control for the locked rate with a propagate-now button and a drift indicator.

split-profiles.service.ts:158–192 (staff edit rewrites lock) · 606–632 (re-stamp on sync) · SplitProfileFormDialog.tsx:769–794 (warning)

SCENARIO S7

Refunds — who funds what, on the way back

Axis 1 · refundFeeBearer: MERCHANT | PLATFORM Axis 2 (subs) · clientRefundFeeBearer: SUBMERCHANT | CLIENT

The invariant: the customer always gets the full refund. The only question is which balance accounts fund it. Ummah deliberately never lets Adyen default to proportional reversal — that would silently claw back Ummah's and the Client's cuts on every refund. Instead every refund carries explicit bearer-aware splits built from the recorded payment legs.

Refund funding decision

FIG 6 · both axes
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":34,"rankSpacing":46,"padding":10}}}%%
flowchart TB
  r(["Refund £100 of a settled payment"]) --> q1{"Ummah's fee portion —
refundFeeBearer?"} q1 -->|"MERCHANT · default"| m1["Store BA funds the FULL £100
Ummah keeps its £1.30
merchant's refund cost = the fee"] q1 -->|"PLATFORM"| m2["Store BA funds £98.70
Ummah returns £1.30 from the liable BA
pro-rata on partial refunds"] m1 --> q2{"Sub-merchant payment?
clientRefundFeeBearer"} m2 --> q2 q2 -->|"SUBMERCHANT · default"| c1["Sub's store also funds
the Client-cut portion —
Client keeps its cut"] q2 -->|"CLIENT"| c2["Client's BA returns
its scaled cut"] classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef client fill:#E8F0FE,stroke:#1A73B8,stroke-width:1.5px,color:#00273A; class r,q1,q2 plain; class m1,m2 ummah; class c1,c2 client;
Axis 1 is admin-set per merchant; axis 2 is the Client's own choice per sub-merchant ("you pay" vs "sub pays") — the one pricing-adjacent lever a Client holds beyond its cut. Over-cap refunds queue for ops approval, where staff may override the bearer per refund.

Tier-1 note. The sheet says "processing fee incl. refunds". Under MERCHANT bearer, Ummah keeping the original fee is economically similar but not the same as charging +£0.10 per refund. If the business wants a literal per-refund fee, that exists — as a REFUND row on the FeeProfile (S9). Both at once would double-charge; pick one (open question Q1).

Works · one arithmetic bugDesign is right. But pro-rata scaling on PLATFORM/CLIENT-bearer refunds divides by the original authorised amount even when the payment was partially captured and the mirror was re-recorded for the captured amount — under-returning the fee portion. Small, real, worth fixing (R3).

adyen-split.service.ts:375–463 · refunds.service.ts:239–293 (totalMinor: txn.amountMinor) · submerchant.controller.ts:219–240

SCENARIO S8

Chargebacks — the recovery waterfall and the fee

Trigger · dispute LOST FeeProfile CHARGEBACK row ring-fenced stores excluded

Two separate money movements. (1) The moment a CHARGEBACK event arrives, the profile-driven chargeback fee books — win or lose, once per dispute, mirroring how Adyen bills the platform. (2) If the dispute is ultimately lost, the disputed amount is recovered into the liable account through a waterfall — because Adyen debited Ummah's liable account immediately and knows nothing of the merchant tree.

Recovery waterfall for a lost £100 dispute

FIG 7 · tranches, in order
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":30,"rankSpacing":42,"padding":10}}}%%
flowchart LR
  lost(["Dispute LOST
£100 owed to the liable BA"]) --> t1["1 · RESERVE
rolling reserve held
for this merchant"] t1 -->|shortfall| t2["2 · STORE
the store that
took the payment"] t2 -->|shortfall| t3["3 · SIBLINGS
richest first —
ring-fenced stores excluded"] t3 -->|shortfall| t4["4 · CLIENT
parent Client's
balance account"] t4 -->|shortfall| t5["5 · ABSORBED
Ummah writes it off
ops notified"] classDef step fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; classDef last fill:#FCE8E6,stroke:#C5221F,stroke-width:1.5px,color:#00273A; classDef start fill:#FEF3DC,stroke:#B26A00,stroke-width:1.5px,color:#00273A; class lost start; class t1,t2,t3,t4 step; class t5 last;
Each tranche is a real internal transfer into the liable account, idempotent per (dispute, source account), sized from live Adyen balances. While the dispute is undecided, the amount is earmarked locally — but only locally: nothing stops the merchant paying those funds out before the loss lands (see S10).

Tier-1 note. The sheet's £8.20 chargeback fee is now representable — a CHARGEBACK FeeProfile row with £8.20 fixed (plus an optional Client cut on top for subs). The alignment doc's “blocked” verdict predates the FeeProfile build and should be updated.

Works · two exposuresThe waterfall is well-engineered (idempotent, resumable, ring-fence aware). Exposures: the dispute earmark never reduces payable balance at Adyen or in the payout path, and a fee booked win-or-lose has no credit path if the merchant later wins. Both are policy decisions to make explicitly, not bugs to hide (R4, Q3).

recovery-plan.ts:23–153 · disputes-intake.service.ts:66–343 · fee-trigger.service.ts:36–113

SCENARIO S9

Non-payment fees — transfers, declines, refunds; payouts free

Actor · Ummah admin only FeeProfile → FeeCharge ledger 31 retries, exp backoff

How it books. No profile assigned ⇒ the merchant is explicitly fee-free. With one, each event books from a rate row (% + fixed, platform leg always, Client leg added on top only when the payer is a sub-merchant) as internal transfers with unique idempotent references. Failed bookings (empty balance) retry automatically and surface in the back-office ledger with manual retry.

Which instrument prices which event

FIG 8 · decision tree
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":30,"rankSpacing":44,"padding":10}}}%%
flowchart TB
  e{"Billable event"} -->|"card payment"| sp["SplitProfile
split inside the authorisation"] e -->|"refund"| rb["Bearer-aware reversal · S7
+ optional REFUND FeeCharge"] e -->|"internal transfer
(chargeFee-flagged)"| tf["FeeProfile TRANSFER
e.g. £0.25"] e -->|"chargeback"| cb["Recovery waterfall · S8
+ FeeProfile CHARGEBACK e.g. £8.20"] e -->|"declined auth"| dc["FeeProfile DECLINE"] e -->|"payout to bank"| po["FREE — decided 2026-08-11
PAYOUT enum reserved"] e -->|"monthly account / KYC"| ab["Ummah absorbs —
platform P&L, not merchant pricing"] classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef ok fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class e plain; class sp,rb,tf,cb,dc ummah; class po,ab ok;
Everything the Tier-1 sheet prices maps to exactly one instrument — the discipline to preserve as the rate card grows. The two "Ummah absorbs" lines are correctly absent from merchant-facing pricing.
SoundClean separation of payment pricing (splits) from event pricing (fee profiles), idempotent booking, honest ledger. One polish item: the fee-profile dialog has no client-side bounds validation — a fat-fingered 100% transfer fee reaches the backend unchecked (R6).

schema.prisma:914–1017 · fee-charge.processor.ts:20–140 · admin-fees.controller.ts:30–147

SCENARIO S10

Failure modes — where the machine bites today

All verified in current code · Aug 2026
FailureWhat happensSeverity
The /v1 capture bypassPOST /v1/payments/:id/capture (sk-key surface) skips the split-aware CapturesService entirely: no status guard, no splits sent or re-recorded, no idempotency key. A partial capture on an explicit-splits payment books the whole amount to the liable account. Same for /cancel vs release.P0
Platform earnings overcountThe earnings SQL counts every Commission leg as Ummah income — including Client-cut legs (Commission with an account, booked to the Client's BA). Every sub-merchant payment inflates reported Ummah revenue by the Client's cut. Same defect in the P&L and fees reports. The correct bucketing already exists in the reconciliation code.P0
Wrong-scheme mirrorfeeMethod comes from the SDK's pre-auth brand hint, never corrected from Adyen's authoritative post-auth data. Stored-card MIT charges send no brand at all ⇒ every MIT on a per-scheme profile records REMAINING rates — guaranteed mismatch noise on subscription-heavy merchants, and mismatch alarms that train ops to ignore alarms.P0
Unsplit sub-merchant invitesThe Client-API and staff sub-merchant invite paths create the sub without a split profile (the portal path resolves one). On activation the store is born profile-less with only a log warning — the sub cannot take a single payment until someone notices and assigns by hand.P1
The self-erasing escape hatchRaw rules[] survive only the first sync. Any later edit auto-resyncs from the managed fields and deletes the raw rules at Adyen — silently. The calculator and the mirror ignore raw rules anyway, so a raw-rules profile mis-quotes and mis-records until reconciliation flags it.P1
Currency-naive fixed feesAll rules are currency: ANY; £0.15 fixed books as €0.15 on a EUR payment. Fine for a GBP-only launch — undefined for anything else. FX-converted settlements additionally settle unverified and skip reserve accrual.P1
Earmark that doesn't holdDisputed amounts are earmarked in local bookkeeping only. Nothing subtracts them from payable balance — a merchant can withdraw funds already claimed by an open dispute, pushing recovery onto siblings, the Client, or the write-off tranche.P1
No commercial ceilings on splitsThe retired FeeSchedule had bounds (≤8%, Amex ≤12%, fixed ≤£1). SplitProfile DTOs accept up to 100% + £1,000,000 fixed with no ceiling — for both admin and Client cut inputs. One typo away from a 25% commission syncing straight to Adyen.P1

checkout.controller.ts:90–106 · payments.service.ts:608–650 · platform-earnings.service.ts:100–115 · checkout.service.ts:341–351 · submerchant.service.ts:413–427 · split-profiles.service.ts:576–586 vs 725–814

Section 6

Amex vs Visa/MC, answered precisely

The questions this document was commissioned to settle, answered from code:

If Amex, Mastercard and Visa carry separate splits and Ummah sets the pricing for merchants and sub-merchants — what happens?

Everything works, at three-bucket resolution. The admin's per-scheme editor writes Visa+Mastercard (one shared rate), Amex, and a catch-all. Adyen matches the rule at authorisation — the platform never has to know the brand in advance for the money to be right. Both Ummah's commission and the Client's cut can differ per scheme when the admin builds the profile. Two limits: Visa and Mastercard cannot be priced differently from each other, and every non-card-scheme method (wallets, Alipay, Pay by Bank) shares the one catch-all rate.

And if the merchant (Client) sets the pricing for its sub-merchants — what happens then?

The Client's cut is flat across schemes: the portal accepts one % + £ pair, full stop. Ummah's per-scheme locked commission still applies underneath (if the admin configured scheme rates on the locked side, they ride along — re-stamped into the Client's profile on every sync). So a Client-priced sub-merchant today has: per-scheme Ummah commission (if admin set it), flat Client cut, passthrough Adyen costs. Nothing breaks; the Client simply has less expressive power than Ummah — by design, though the flat-cut restriction is a product choice worth revisiting (R10), since staff can already add scheme rates to the same profile.

Where does the money knowledge live — before or after the card is seen?

Both, and they must agree. Before: the SDK's BIN-lookup brand hint picks which rates the platform records (the mirror). At Adyen: rule matching on the actual payment method decides what is booked. The reconciliation gate compares the two per payment. This dual bookkeeping is the platform's honesty mechanism — which is why the wrong-scheme mirror defects in S10 matter more than they look: they erode the one signal that proves pricing correctness daily.

Rule matching at Adyen, and the bucket collision

FIG 9 · card → rate
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":26,"rankSpacing":48,"padding":10}}}%%
flowchart LR
  visa["Visa"] --> vmc["rule visa · rule mc
VISA_MC rate
e.g. 1.15% + £0.15"] mc["Mastercard"] --> vmc amex["American Express"] --> ax["rule amex
AMEX rate
e.g. 2.75% + £0.15"] ap["Apple Pay"] --> any["rule ANY — catch-all
REMAINING rate
one rate for all of these"] gp["Google / Samsung Pay"] --> any ali["Alipay · WeChat Pay"] --> any pbb["Pay by Bank"] --> any other["Any future method"] --> any classDef card fill:#F1F4F8,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; classDef rate fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef clash fill:#FEF3DC,stroke:#B26A00,stroke-width:1.5px,color:#00273A; class visa,mc,amex,ap,gp,ali,pbb,other card; class vmc,ax rate; class any clash;
The amber node is the Tier-1 blocker: wallets that should price at 0.20% and APMs that should price at 3.00% are forced to share one rate. The catch-all itself is non-negotiable — it is what stops an unmatched payment booking 100% to the liable account — the fix is more buckets in front of it, never removing it.

Section 7

The pricing models — industry standard, and what IC++ means on Ummah

Three models decide who bears the card-cost risk: blended, IC+ / IC++, and passthrough. This section defines them the way the industry (Stripe, Adyen) uses them, kills the "charged twice in a marketplace" myth, and — the important part — works out what a true IC++ product would actually look like on Ummah's split engine, given that the splits themselves can't itemise the real cost. The comprehensive internal reference is the Fees, Card Types, Currencies & Pricing bible — this section builds on it.

How the industry prices

Stripe's default is flat/blended — one "% + fixed" per card, tiered by origin (UK online standard 1.5% + 20p, premium 2.8% + 20p; US 2.9% + $0.30), where Stripe absorbs the interchange variance and prices the average. It offers Interchange Plus only on custom/enterprise terms, negotiated on request — not the self-serve default. Adyen (Ummah's acquirer) is natively interchange++/passthrough underneath. The four models:

The four acquiring pricing models — one axis: who bears the card-cost variance, how transparently
ModelMerchant is billedBears the Amex riskTransparency
BlendedOne flat rate for every cardAcquirer / platform — prices the average, loses on AmexOne line; real cost hidden
IC+ (Interchange Plus)Interchange at cost + one combined markup (scheme fee folded into the markup)MerchantInterchange visible; scheme not split out
IC++ (Interchange Plus Plus)Interchange at cost + scheme fee at cost, itemised + the acquirer markup (the only kept part)MerchantMost transparent — every component a line
PassthroughThe umbrella term: merchant pays the acquirer's actual cost. IC+ and IC++ are itemised passthrough; "plain" passthrough passes cost through with a single blended markup, not itemisedMerchantVaries

The relationships that matter: passthrough is the family; IC++ is its fully-itemised member (IC++ is passthrough, broken into interchange + scheme + markup lines). IC+ folds the scheme fee into the markup; IC++ passes it through separately. Blended is the opposite of all three — a fixed rate where the acquirer eats the variance.

In a marketplace, is the merchant charged a fee and then the sub-merchant charged the same fee again?

No — the processing fee is charged exactly once, then a commission is split off on top. A marketplace payment has three different takers, each taking once: (1) the acquirer's processing cost (Adyen's real fee), paid once; (2) the platform's commission (Ummah's cut) — a markup on top, not a second processing fee; (3) the marketplace client's cut, once. Nobody is double-charged. On a £100 sub-merchant payment the sub-merchant nets the remainder after those three are taken — the same money is never processed-fee'd twice.

What a true IC++ would look like on Ummah — and the settlement answer

Here is the crux you put your finger on: on the splits, Ummah cannot itemise the real cost — because interchange is only known at settlement. At authorisation the card networks haven't determined interchange yet, so there is no real number to put in a split leg. That constraint shapes everything, and it means IC++ on Ummah is inherently two-phase:

IC++ on Ummah is two-phase: markup at auth, cost breakdown at settlement

FIG 8 · what books when
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"12.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":24,"rankSpacing":34,"padding":10}}}%%
flowchart TB
  subgraph AUTH["At authorisation — amounts you KNOW"]
    p(["£100 payment"]) --> mk["Ummah markup — the '++'
fixed Commission leg → liable BA
e.g. 0.20% + £0.10 = £0.30"] p --> net["Merchant net
Remainder → merchant BA"] p --> cost["Adyen cost leg → merchant BA
type set, amount unknown
merchant shown an ESTIMATE"] end subgraph SETTLE["At settlement (T+1/T+2) — amounts Adyen COMPUTES"] a["Adyen deducts the ACTUAL cost
from the merchant BA:
interchange + scheme + Adyen markup"] --> ing["Ummah ingests the settled
cost breakdown (settlement detail)"] ing --> stmt["Itemised IC++ statement to merchant:
interchange £x · scheme £y · markup £z · Ummah £0.30"] end cost -.->|"real number lands here"| a classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef sub fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef adyen fill:#00273A,stroke:#00273A,color:#FFFFFF; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class mk ummah; class net sub; class a,ing adyen; class p,cost,stmt plain;
The direct answer to "would we get our commission at final settlement?" — no: Ummah's commission (the markup, the "++") books at authorisation as a fixed Commission leg; you always know your own cut upfront. What arrives at settlement is the actual cost breakdown — and surfacing that as itemised lines is exactly what turns plain passthrough into IC++.

So the mechanics resolve cleanly:

  • Ummah's markup — booked at auth. Your commission is a fixed Commission split leg to the liable account (a % + fixed you set). It is deterministic and does not wait for settlement.
  • The card cost — passed through, settled later. The Adyen cost leg (PaymentFee, or Adyen's granular acquiring-fee types) tells Adyen to debit its actual cost from the merchant's balance account. You never set that amount — Adyen fills it at settlement, because that is when interchange is known.
  • Itemisation is a settlement-time read, not a checkout-time split. To show the merchant "interchange £0.30 · scheme £0.02 · Adyen markup £0.13," Ummah must ingest Adyen's settled cost breakdown after the fact. The split legs at checkout carry the structure; settlement supplies the numbers.
  • At checkout the merchant sees an estimate. Because the cost is unknown until settlement, the "you'll receive ~£99.15" shown at pay time is an estimate; the statement reconciles to the real figure a day or two later. (This is the same estimate-vs-settled rule as the FX and passthrough discussions.)
IC++ SCENARIO · A

£100 on a UK Visa credit card — itemised

Ummah markup 0.20% + £0.10cost passed to merchantillustrative costs
LineAmountBookedCharacter
Customer pays£100.00auth—
Interchange (UK credit)−£0.30settlementat cost → merchant BA
Scheme fee−£0.02settlementat cost → merchant BA
Adyen markup + fixed−£0.20settlementat cost → merchant BA
Ummah markup (the ++)−£0.30authCommission → liable BA
Merchant nets~£99.18settledsees every line
The IC++ promiseThe merchant pays the true cost of a cheap card and keeps almost all of £100 — plus a transparent, auditable breakdown. Ummah's £0.30 is clean, predictable margin regardless of the card.
IC++ SCENARIO · B

The same £100 on Amex — the merchant bears it, transparently

Ummah markup 0.20% + £0.10Amex ~3.5% cost
LineAmountCharacter
Customer pays£100.00—
Amex acquiring cost (interchange-equivalent + scheme)−£3.50at cost → merchant BA (settlement)
Ummah markup (the ++)−£0.30Commission → liable BA
Merchant nets~£96.20sees the £3.50 as its own line
Why IC++ beats blended hereUnder blended, Ummah would have to charge one rate that clears this £3.50 Amex cost on every card — overcharging Visa to cover Amex, or losing money on Amex (the trap the calculator warns about). Under IC++, the merchant simply bears the real Amex cost on Amex, sees it itemised, and Ummah's margin is invariant. This is why sophisticated merchants prefer IC++.

What Ummah must build to offer it

Build · 1

Ingest the settled cost breakdown

Consume Adyen's settlement-detail / cost data so the actual interchange, scheme fee and Adyen markup land per transaction. Today the PaymentFee split leg is a 0-value marker and is excluded from reconciliation — the real cost lives only in Adyen's ledger. This is the audit's R9 and the single hard prerequisite for IC++.

Build · 2

Store the components, not a blob

Add per-transaction cost columns (interchange, scheme, markup) alongside a commissionMinor — the same denormalisation the earnings fix needs — so IC++ statements are an indexed read, not a JSON scan.

Build · 3

Emit the granular Adyen cost types (optional)

Ummah's split type union already declares AcquiringFees / AdyenMarkup / AdyenFees but emits none — it uses the single PaymentFee. Using the granular types lets Adyen attribute the cost by component; combined with Build 1 it produces a fully-itemised statement.

Build · 4

A merchant IC++ statement + honest checkout copy

Surface the itemised breakdown post-settlement, and label the checkout figure an estimate ("final at settlement, at cost"). Never show a passthrough/IC++ estimate as a guaranteed payout.

Where Ummah is today: it runs blended (feesBorneBy = PLATFORM) and passthrough (feesBorneBy = SUBMERCHANT) — the cost is genuinely passed to the merchant under passthrough, but as one lump, not itemised, and never captured from settlement. That is passthrough with a blended markup, economically close to IC++ but not IC++. Turning it into true IC++ is Builds 1–4 above — nothing in the split shape changes, only the settlement feed and the statement.

Section 8

Gap analysis — the Tier-1 rate card vs the engine

Row-by-row, can the platform charge what the business wants to sell?

Tier-1 lineStatusDetail
Processing fee £0.10/txnWorkscommissionFixedMinor = 10. The "incl. refunds" clause needs a decision, not code (Q1).
Blended headline (1.15% + £0.15)WorksSingle-rate or per-scheme profile; Amex guard in the calculator.
Interchange / scheme / FX passthroughWorksfeesBorneBy = SUBMERCHANT — Adyen deducts actuals from the merchant.
Amex distinct rate (2.75%)WorksAMEX method group, UI-managed.
Transfer fee £0.25 · chargeback £8.20 · decline · refund feesWorksFeeProfile rows, booked as internal transfers with a ledger. Built 2026-08-11.
Payout feeFree by decisionTrigger built and removed same day; enum reserved if the decision reverses.
9-column per-method grid (wallets 0.20% vs Alipay 3.00%)BlockedThree buckets cannot hold five rates. The escape hatch technically can — and self-erases on the next edit. The gap to close first.
Amex US OptBlue (3.30%)BlockedNeeds a card-region condition the model doesn't carry (Adyen-side availability unconfirmed).
Risk surcharge £0.05, optionalWorkaround onlyA second profile with the surcharge folded into fixed — not per-merchant toggleable, not separately reportable.
Locked + passthrough mixed per componentCoarsefeesBorneBy flips all Adyen costs at once. Sufficient for this sheet; insufficient for a locked-scheme-fee variant.
Non-GBP fixed amountsUndefinedcurrency: ANY books £0.15 as 0.15 of anything. GBP-only until Q5 is answered.
Realised margin (the 0.64%)MissingNo actuals feed, no margin ledger. The commercial margin exists in the calculator's estimates and nowhere else.
Volume ladders / tiersMissingNo pricing object supports them; flat % + fixed only.

Section 9

Architect's recommendations

The verdict first: the pricing architecture is right. Splitting at authorisation on Adyen's rails, one template per store, an admin-locked platform margin with a Client-composable cut, and a mirror-and-reconcile audit loop — that is the correct shape for this business, and it is proven against the processor's own ledger. Do not redesign it. The work is to close the correctness holes (P0), lift the three-bucket ceiling (P1), and build the margin telemetry the business already believes it has (P2).

P0 · R1

Close the /v1 capture & cancel bypass

Route POST /v1/payments/:id/capture and /cancel through the split-aware CapturesService — same guards, same splits, same idempotency. Until then, one partial capture from an API integration books an entire payment to the liable account. A five-line controller change plus tests.

R2

Fix the earnings SQL before anyone trusts a revenue number

Exclude Commission-with-account legs from "collected" in platform-earnings and commissionFromSplits in the report registry, mirroring the bucketing reconciliation already does. Today every marketplace payment overstates Ummah's revenue by the Client's cut — the worst possible bug to discover during due diligence.

R3

Make the mirror truthful

On AUTHORISATION, recompute feeMethod + splitsApplied from Adyen's authoritative additionalData (paymentMethodVariant) instead of trusting the SDK hint — which also fixes the guaranteed-wrong MIT mirror. While there, scale refund pro-rata by captured amount, not authorised (S7).

P1 · R4

One invite spine, fail-closed on pricing

Make every sub-merchant invite path resolve a split profile the way the portal path does (explicit choice → parent default → 422). A sub that activates unsplit is a support ticket wearing a growth metric. Fold the marketplace gates (isMarketplace vs canCreateSubMerchants) into one predicate while there.

R5

Extend the method groups — retire the escape hatch for Tier-1

Add WALLETS (applepay, googlepay, samsungpay), APM_HIGH (alipay, wechatpay), PAY_BY_BANK, and optionally AMEX_US to SplitMethodGroup; extend the rule generator, splitGroupFor, the BO editor, and the calculator together (they must stay in lockstep — that is the invariant that keeps the mirror honest). This single change makes the whole Tier-1 grid UI-manageable. Then make raw rules[] either survive re-sync or refuse to save — a one-shot self-erasing hatch is worse than none.

R6

Commercial bounds everywhere money is typed

Port the retired FEE_BOUNDS discipline to SplitProfile and FeeProfile DTOs (server-side) and their dialogs (client-side): commission ≤ 8% (Amex ≤ 12%), fixed ≤ £1, Client cut ≤ a per-Client ceiling the admin sets when enabling sub-merchants. Guardrails are what make delegated pricing (S5) safe to scale.

R7

Make rates visible: a contract-rate surface + the locked-rate control

Merchants currently learn their price from a calculator and raw split legs. Ship (a) a portal "Your rates" page rendered from the live SplitProfile (+ FeeProfile rows), (b) an endpoint exposing the admin-set locked rate so a fresh Client sees Ummah's cut before creating its first split, and (c) a dedicated admin control for subMerchantCommission* with resync-all and per-profile drift indicators (S6). Transparency is a pricing feature, not a docs page.

R8

Risk surcharge as a field, not a fork

One optional riskSurchargeFixedMinor on the profile (or per store), folded into generated rules and reported as its own line. Kills the two-profile workaround and makes the surcharge separately auditable — which is the actual business requirement (Q2).

P2 · R9

Ingest settlement actuals; open the margin ledger

Consume Adyen's settlement-detail reports to land per-transaction interchange, scheme fee and markup against the Transaction. Only then does "Ummah margin 0.64%" become a ledger fact instead of a calculator estimate — and IC++ pricing, per-merchant profitability, and blended-loss alerts on real traffic all become possible. Pair with a liable-account ledger that separates earned commission, absorbed costs, chargeback write-offs and fee income (the pieces exist in RecoveryEvent and FeeCharge; give them one view).

R10

Grow Client pricing power deliberately

Once R5 and R6 land: per-scheme Client cuts in the portal (the data model already supports it), and per-currency fixed amounts if Tier-1 goes beyond GBP (Q5). Sequenced last on purpose — delegation expands only after guardrails and buckets exist.

R11

Delete the dead pricing generation

The retired FeeSchedule engine still computes a frozen snapshot every session, feeds nothing reachable, and duplicates fee math in the worker by comment-discipline. Excise the snapshot for profile stores, remove the legacy splitConfig write path, and keep exactly one pricing truth. Dead-but-load-bearing code is where the next pricing bug is already living.

Target state — the same machine, with the holes closed

FIG 10 · after R1–R9
%%{init:{"theme":"base","themeVariables":{"fontFamily":"Bricolage Grotesque, Inter, Segoe UI, sans-serif","fontSize":"13.5px","primaryColor":"#FFFFFF","primaryTextColor":"#00273A","primaryBorderColor":"#D2D8DE","lineColor":"#8195A1","clusterBkg":"#F7F8FA","clusterBorder":"#E5E8EC","edgeLabelBackground":"#FFFFFF"},"flowchart":{"curve":"basis","nodeSpacing":34,"rankSpacing":50,"padding":10}}}%%
flowchart LR
  subgraph CONFIG["Pricing configuration"]
    sp["SplitProfile
6 method groups · bounds ·
surcharge field · currency-scoped fixed"] fp["FeeProfile
bounded rates"] end subgraph FLOW["Every payment"] pay["Auto-split at auth"] --> mirror["Mirror from Adyen's
authoritative method data"] mirror --> recon["Reconcile — quiet
because it is now truthful"] end subgraph TRUTH["Margin telemetry"] rep["Settlement-detail ingestion
actual interchange + scheme + markup"] ledger["Liable-account ledger
earned · absorbed · written-off · fees"] end sp --> pay fp --> ledger recon --> ledger rep --> ledger ledger --> price["Pricing decisions on facts:
real margin per merchant, per scheme"] price -.->|"re-price"| sp classDef ummah fill:#FBE5D6,stroke:#E86C2B,stroke-width:1.5px,color:#00273A; classDef ok fill:#E6F4EA,stroke:#1E8E3E,stroke-width:1.5px,color:#00273A; classDef plain fill:#FFFFFF,stroke:#D2D8DE,stroke-width:1.5px,color:#00273A; class sp,fp ummah; class pay,mirror,recon plain; class rep,ledger,price ok;
The closing loop — margin facts feeding re-pricing — is what turns the pricing stack from configuration into product. "Pricing is a product decision, not a code change" only becomes fully true when the decision has real numbers underneath it.

Section 10

Open business questions

Five decisions block final Tier-1 configuration. None is engineering's to make alone:

  • Q1 · "Processing fee incl. refunds." Literal +£0.10 per refund (⇒ a REFUND FeeProfile row), or "we keep the payment-time fee on refund" (⇒ MERCHANT bearer, already the default)? Both at once double-charges.
  • Q2 · Risk surcharge. Opt-in per merchant or per store? Must it report separately? (Determines whether R8 is a field or also a report line.)
  • Q3 · Chargeback £8.20. Always, or only when Adyen bills? And if a dispute is later won, is the fee credited back? (Today: booked win-or-lose, no credit path.)
  • Q4 · Non-card cost inputs. Interchange for Alipay/WeChat/Amex/Pay-by-Bank and 3DS pricing are still TBC with Adyen — the calculator's cost card needs real numbers before quotes on those methods mean anything.
  • Q5 · Currency scope. Is Tier-1 GBP-only? If not, fixed amounts need per-currency values (schema + rule generation work) — today £0.10 books as €0.10.